package org.ydeity.lucene.convert;

import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.document.*;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.spatial3d.Geo3DDocValuesField;
import org.apache.lucene.spatial3d.Geo3DPoint;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.springframework.util.StringUtils;
import org.ydeity.lucene.configuration.LuceneDocumentProperties;
import org.ydeity.lucene.configuration.LuceneFieldProperties;
import org.ydeity.lucene.configuration.LuceneProperties;
import org.ydeity.lucene.exception.LuceneException;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;

@Slf4j
public class MappingLuceneConvert implements LuceneConvert {

    private Map<Class<?>,LuceneDocumentProperties> mappingContext;

    private LuceneProperties luceneProperties;

    private static final String SET_PREFIX = "set";
    private static final String GET_PREFIX = "get";
    private static final String BOOLEAN_GET_PREFIX = "is";

    public MappingLuceneConvert(LuceneProperties luceneProperties){
        this.mappingContext = new HashMap<>();
        this.luceneProperties = luceneProperties;
    }

    @Override
    public Map<Class<?>, LuceneDocumentProperties> getMappingContext() {
        return null;
    }

    @Override
    public LuceneDocumentProperties getLuceneDocumentProperties(Class<?> clazz) {
        return this.mappingContext.computeIfAbsent(clazz, m -> new LuceneDocumentProperties(clazz, this.luceneProperties));
    }

    @Override
    public <T> Document getLuceneDocument(Class<?> clazz, T t) {
        return this.getDocument(t,null);
    }

    public <T> Document getExistDocument(Object obj, T t) {
        return this.getDocument(t,obj);
    }

    private <T> Document getDocument(T t,Object obj) {
        LuceneDocumentProperties documentProperties = this.getLuceneDocumentProperties(t.getClass());
        Map<String, LuceneFieldProperties> fieldMapping = documentProperties.getFieldMapping();

        Document doc = new Document();
        for (String property : fieldMapping.keySet()) {
            LuceneFieldProperties fieldProperties = fieldMapping.get(property);

            Object value = this.getValue(property, t,fieldProperties.getParamType());
            if(value == null && obj != null){
                value = this.getObjVal(obj, property);
            }

            if(value == null){
                continue;
            }

            String val = String.valueOf(value);
            switch (fieldProperties.getFieldType()){
                case Text:
                    doc.add(new TextField(property, val,fieldProperties.isStore()));
                    break;
                case Integer:
                    Integer integerValue = Integer.valueOf(String.valueOf(value));
                    doc.add(new IntPoint(property, integerValue));
                    doc.add(new StringField(property, val,fieldProperties.isStore()));
                    break;
                case Date:
                case Long:
                    Long longValue = Long.valueOf(String.valueOf(value));
                    doc.add(new LongPoint(property, longValue));
                    doc.add(new StringField(property, val,fieldProperties.isStore()));
                    break;
                case Double:
                    Double doubleValue = Double.valueOf(String.valueOf(value));
                    doc.add(new DoublePoint(property, doubleValue));
                    doc.add(new StringField(property, val,fieldProperties.isStore()));
                    break;
                case GEO:
                    String[] location = val.split(",");
                    Double latitude = Double.valueOf(location[0]);
                    Double longitude = Double.valueOf(location[1]);
                    Geo3DPoint point = new Geo3DPoint(property, latitude, longitude);
                    doc.add(new StringField(property, val,fieldProperties.isStore()));
                    doc.add(point);
                    doc.add(new Geo3DDocValuesField(property, new GeoPoint(PlanetModel.WGS84, latitude * Math.PI / 180.0, longitude * Math.PI / 180.0)));
                    break;
                default:
                    if(StringUtils.isEmpty(val)) {
                        continue;
                    }
                    FieldType type = new FieldType();
                    type.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
                    type.setTokenized(false);
                    type.setStored(true);
                    Field field = new Field(property, val, type);

                    doc.add(field);
                    break;
            }
        }

        return doc;
    }

    private Object getObjVal(Object obj,String property){
        if(obj == null) {
            return null;
        }

        Method method = null;
        try {
            method = obj.getClass().getMethod(GET_PREFIX + getFirstString(property));
        } catch (NoSuchMethodException e) {
            throw new LuceneException(e.getMessage());
        }
        Object invoke = null;
        try {
            invoke = method.invoke(obj);
        } catch (IllegalAccessException | InvocationTargetException e) {
            log.error(e.getMessage());
        }
        return invoke;
    }

    /**
     * entity 转 document
     * @param t
     * @param <T>
     * @return
     */
    @Override
    public <T> Document getLuceneDocument(T t) {
        return this.getLuceneDocument(t.getClass(),t);
    }

    public String getIdProperty(Class<?> clazz){
        LuceneDocumentProperties luceneDocumentProperties = this.getLuceneDocumentProperties(clazz);
        return luceneDocumentProperties.getIdProperties().getProperty();
    }

    private <T> Object getValue(String property,T t,Class<?> paramType){
        String methodName = null;
        if(paramType.equals(boolean.class)){
            methodName = BOOLEAN_GET_PREFIX + getFirstString(property);
        } else {
            methodName = GET_PREFIX + getFirstString(property);
        }

        Method method = null;
        try {
            method = t.getClass().getMethod(methodName);
        } catch (NoSuchMethodException e) {
            throw new LuceneException(e.getMessage());
        }

        Object value = null;
        try {
            value = method.invoke(t);
        } catch (IllegalAccessException | InvocationTargetException e) {
            log.error(e.getMessage());
        }

        if(value == null){
            return null;
        }

        if(paramType.equals(Boolean.class) || paramType.equals(boolean.class)){
            boolean b = (boolean) value;
            if(b){
                return 1;
            } else {
                return 0;
            }
        }

        if(paramType.equals(Date.class)){
            Date date = (Date) value;
            return date.getTime();
        }

        if(paramType.equals(LocalDate.class)){
            LocalDate localDate = (LocalDate) value;
            return localDate.atStartOfDay(ZoneOffset.ofHours(8)).toInstant().toEpochMilli();
        }

        if(paramType.equals(LocalDateTime.class)){
            LocalDateTime localDateTime = (LocalDateTime) value;
            return localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
        }

        return value;
    }

    private static String getFirstString(String str) {
        byte[] bytes = str.getBytes();
        bytes[0] = (byte) (bytes[0] - 32);
        return new String(bytes);
    }

    public <T> List<T> convertTopDoc(Class<?> clazz, TopDocs topDocs) {
        if(topDocs.totalHits == 0) {
            return Collections.emptyList();
        }

        List<T> list = new ArrayList<>();
        for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
            Object obj = this.newInstance(clazz);

            LuceneDocumentProperties documentProperties = this.getLuceneDocumentProperties(clazz);
            Map<String, LuceneFieldProperties> fieldMapping = documentProperties.getFieldMapping();

            for (String property : fieldMapping.keySet()) {
                String value = null;
                try {
                    value = documentProperties.getIndexSearcher().doc(scoreDoc.doc).get(property);
                    if(StringUtils.isEmpty(value)) {
                        continue;
                    }
                } catch (IOException e) {
                    log.error(e.getMessage());
                }

                LuceneFieldProperties fieldProperties = fieldMapping.get(property);

                Method method = null;
                try {
                    method = clazz.getMethod(SET_PREFIX + getFirstString(property), fieldProperties.getParamType());
                    method.invoke(obj, this.getObjVal(value,fieldProperties.getParamType()));
                } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                    log.error(e.getMessage());
                }
            }
            list.add((T)obj);
        }

        return list;
    }

    public Object getObjVal(String value, Class<?> paramType) {
        if(StringUtils.isEmpty(value)){
            return null;
        }

        if(paramType.equals(String.class)){
            return value;
        }

        if(paramType.equals(Integer.class) || paramType.equals(int.class)){
            return Integer.valueOf(value);
        }

        if(paramType.equals(Long.class) || paramType.equals(long.class)){
            return Long.valueOf(value);
        }

        if(paramType.equals(Short.class) || paramType.equals(short.class)){
            return Short.valueOf(value);
        }

        if(paramType.equals(Double.class) || paramType.equals(double.class)){
            return Double.valueOf(value);
        }

        if(paramType.equals(Boolean.class) || paramType.equals(boolean.class)){
            Integer b = Integer.valueOf(value);
            if(b.equals(1)){
                return Boolean.TRUE;
            } else {
                return Boolean.FALSE;
            }
        }

        if(paramType.equals(BigDecimal.class)){
            return BigDecimal.valueOf(Double.valueOf(value));
        }

        if(paramType.equals(Date.class)){
            return new Date(Long.valueOf(value));
        }

        if(paramType.equals(LocalDateTime.class)){
            return new Date(Long.valueOf(value)).toInstant().atOffset(ZoneOffset.ofHours(8)).toLocalDateTime();
        }

        if(paramType.equals(LocalDate.class)){
            return Instant.ofEpochMilli(Long.valueOf(value)).atZone(ZoneOffset.ofHours(8)).toLocalDate();
        }

        return null;
    }

    private Object newInstance(Class<?> clazz){
        try {
            Object instance = clazz.newInstance();
            return instance;
        } catch (InstantiationException | IllegalAccessException e) {
            throw new LuceneException("Instance init fail :" + clazz);
        }
    }

    public <T> Serializable getId(T t){
        String idProperty = this.getIdProperty(t.getClass());

        try {
            Method method = t.getClass().getMethod(GET_PREFIX + getFirstString(idProperty));
            Object v = method.invoke(t);
            return (Serializable) v;
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            log.error(e.getMessage());
        }

        return null;
    }
}
